package com.legsem.jhosync.ws;

import java.io.IOException;
import java.net.URL;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.sql.DataSource;

import org.scannotation.WarUrlFinder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.legsem.jhosync.ConfigurationException;
import com.legsem.jhosync.Constants;
import com.legsem.jhosync.Server;
import com.legsem.jhosync.SourceConfig;
import com.legsem.jhosync.face.Application;
import com.legsem.jhosync.face.JhosyncApplication;
import com.legsem.jhosync.face.JhosyncSource;
import com.legsem.jhosync.face.SourceAdapter;
import com.legsem.jhosync.model.Model;
import com.legsem.jhosync.model.ThreadLocalConnection;
import com.legsem.jhosync.model.User;
import com.legsem.jhosync.util.ClasspathUtil;

/**
 * Servlet Filter implements a ThreadLocal session programming model. This
 * reduces the amount of session management code downstream.
 * <p/>
 * On startup the filter also discovers sources and applications and store their
 * references in the servlet context.
 */
public class JhosyncFilter implements Filter {

    /** Identifies the JDBC data source in the servlet configuration. */
    public static final String JNDI_DATASOURCE_NAME = "jhosync.datasource.name";

    private final Logger logger = LoggerFactory.getLogger(JhosyncFilter.class);

    /** JDBC data source. */
    private DataSource dataSource;

    /**
     * Instantiate the datasource only once for all subsequent servlet
     * executions to use.
     * <p/>
     * Locate applications sand sources from the classpath and keep a reference.
     * 
     * @see Filter#init(FilterConfig)
     */
    public void init(FilterConfig fConfig) throws ServletException {
        initDatabase(fConfig.getServletContext());
        URL classesPath = WarUrlFinder.findWebInfClassesPath(fConfig
                .getServletContext());
        if (classesPath == null) {
            logger.error("Unable to locate WEB-INF/classes");
        } else {
            initApplication(fConfig.getServletContext(), classesPath);
            initSources(fConfig.getServletContext(), classesPath);
        }
    }

    protected void initDatabase(final ServletContext servletContext)
            throws ServletException {
        String jndiDsName = servletContext
                .getInitParameter(JNDI_DATASOURCE_NAME);

        logger.info("Configuring data source using {}", jndiDsName);

        try {
            Context initCtx = new InitialContext();
            Context envCtx = (Context) initCtx.lookup("java:comp/env");

            dataSource = (DataSource) envCtx.lookup(jndiDsName);
            initDatabaseSchema();

        } catch (NamingException e) {
            throw new ServletException(e);
        }
    }

    /**
     * If the database is being opened for the first time, we need to create the
     * tables, etc..
     * 
     * @throws ServletException if creating schema fails
     */
    protected void initDatabaseSchema() throws ServletException {
        Connection conn = null;
        try {
            conn = dataSource.getConnection();
        } catch (SQLException e) {
            throw new ServletException(e);
        }

        ThreadLocalConnection.set(conn);
        User userAdmin = null;
        try {
            userAdmin = User.find(Constants.ADMIN_LOGIN, false);
            if (userAdmin != null) {
                try {
                    conn.close();
                } catch (Exception e) {
                    throw new ServletException(e);
                }
                ThreadLocalConnection.set(null);
                return;
            }
        } catch (SQLException e) {
        }

        logger.info("Creating database schema");
        try {
            Model.initSchema();
            conn.commit();
        } catch (SQLException e) {
            try {
                if (conn != null && !conn.isClosed()) {
                    conn.rollback();
                }
            } catch (SQLException e1) {
                e1.printStackTrace();
            }
            throw new ServletException(e);
        } finally {
            if (conn != null) {
                try {
                    conn.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }

        }
        ThreadLocalConnection.set(null);
    }

    /**
     * Locate the developers Application code and store an instance in the
     * application-wide context.
     * 
     * @param servletContext the application-wide context
     */
    protected void initApplication(final ServletContext servletContext,
            URL classesPath) {
        try {
            List < Class < Application >> classes = locateClasses(classesPath,
                    JhosyncApplication.class.getName());
            if (classes != null && classes.size() > 0) {
                if (classes.size() > 1) {
                    logger.warn(
                            "There are more than one implementation of {} on the classpath. Will use {}",
                            Application.class.getName(), classes.get(0));
                }
                Class < Application > appClass = classes.get(0);
                JhosyncApplication annotation = appClass
                        .getAnnotation(JhosyncApplication.class);
                servletContext.setAttribute(
                        Server.APPLICATION_IMPLEMENTATION_KEY,
                        appClass.newInstance());
                servletContext.setAttribute(Server.APPLICATION_NAME_KEY,
                        annotation.name());
            }
        } catch (InstantiationException e) {
            logger.error("Failed to instanciate Application implementation", e);
        } catch (IllegalAccessException e) {
            logger.error("Failed to instanciate Application implementation", e);
        }
    }

    /**
     * Default constructor.
     */
    public JhosyncFilter() {
    }

    /**
     * @see Filter#destroy()
     */
    public void destroy() {
        dataSource = null;
    }

    /**
     * An EntityManager (Session) is created for each request. It is then stored
     * in a thread local. This is convenient as we are guaranteed that the
     * downstream servlet code runs within the same thread as this one.
     * <p/>
     * Transaction management is handled here to relieve downstream code.
     * 
     * @see Filter#doFilter(ServletRequest, ServletResponse, FilterChain)
     */
    public void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException {

        logger.debug("Received {} request {}",
                ((HttpServletRequest) request).getMethod(),
                getUrl((HttpServletRequest) request));

        Connection conn = null;
        try {
            conn = dataSource.getConnection();
            conn.setAutoCommit(false);
            conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
            ThreadLocalConnection.set(conn);
            logger.debug("opening connection: " + conn);

            StatusExposingServletResponse decoratedResponse = new StatusExposingServletResponse(
                    (HttpServletResponse) response);
            chain.doFilter(request, decoratedResponse);
            if (!conn.isClosed()) {
                if (decoratedResponse.getStatus() == HttpServletResponse.SC_OK) {
                    conn.commit();
                } else {
                    conn.rollback();
                    logger.warn(
                            "transaction rolled back due to status code {}",
                            decoratedResponse.getStatus());
                }
            }
        } catch (Exception e) {
            try {
                if (conn != null && !conn.isClosed()) {
                    conn.rollback();
                    logger.warn("transaction rolled back due to ", e);
                }
            } catch (SQLException e1) {
                logger.warn("failed to rollback transaction ", e);
            } finally {
                logger.error("abnormal termination", e);
            }
        } finally {
            logger.debug("closing connection: " + conn);
            try {
                if (conn != null && !conn.isClosed()) {
                    conn.close();
                }
            } catch (SQLException e) {
                logger.warn("failed to close connection ", e);
            }
            ThreadLocalConnection.set(null);
        }
    }

    /**
     * Locate the developers source code and store source configuration data in
     * an application-wide scope.
     * 
     * @param servletContext the application-wide context
     */
    protected void initSources(final ServletContext servletContext,
            URL classesPath) {
        Map < String, SourceConfig > sourceConfigs = new HashMap < String, SourceConfig >();

        List < Class < SourceAdapter >> classes = locateClasses(classesPath,
                JhosyncSource.class.getName());
        if (classes != null) {
            for (Class < SourceAdapter > sourceClass : classes) {
                SourceConfig sourceConfig = new SourceConfig();
                JhosyncSource annotation = sourceClass
                        .getAnnotation(JhosyncSource.class);
                sourceConfig.setName(annotation.name());
                sourceConfig.setPollInterval(annotation.poll_interval());
                sourceConfig.setSourceClass(sourceClass);

                sourceConfigs.put(sourceConfig.getName(), sourceConfig);

            }
        }

        servletContext.setAttribute(Server.SOURCE_CONFIGS_KEY, sourceConfigs);
    }

    /**
     * Lookup the classpath for annotated classes.
     * 
     * @param classesPath the location (wuthin the classpath) to look into
     * @param annotationClassName the annotation class name
     * @return a list of classes annotated with the sought annotation
     */
    protected <T> List < Class < T >> locateClasses(URL classesPath,
            String annotationClassName) {
        try {
            logger.info("Looking for classes annotated with {} in {}",
                    annotationClassName, classesPath);
            List < Class < T >> classes = ClasspathUtil
                    .getAnnotatedImplementationClasses(annotationClassName,
                            new URL[] { classesPath });
            if (classes.size() == 0) {
                logger.error(
                        "Unable to locate a class annotated with {} on the classpath",
                        annotationClassName);
            }
            return classes;
        } catch (ConfigurationException e) {
            logger.error("Initialization failure", e);
            return null;
        }
    }

    protected String getUrl(HttpServletRequest req) {
        String scheme = req.getScheme(); // http
        String serverName = req.getServerName(); // hostname.com
        int serverPort = req.getServerPort(); // 80
        String contextPath = req.getContextPath(); // /mywebapp
        String servletPath = req.getServletPath(); // /servlet/MyServlet
        String pathInfo = req.getPathInfo(); // /a/b;c=123
        String queryString = req.getQueryString(); // d=789

        // Reconstruct original requesting URL
        String url = scheme + "://" + serverName + ":" + serverPort
                + contextPath + servletPath;
        if (pathInfo != null) {
            url += pathInfo;
        }
        if (queryString != null) {
            url += "?" + queryString;
        }
        return url;
    }

}
